package com.googlecode.connectlet.crypto.rsa;

import java.math.BigInteger;
import java.security.interfaces.RSAKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPublicKey;

import com.googlecode.connectlet.crypto.random.RC4PrngCipher;
import com.googlecode.connectlet.util.Bytes;

public class RSA {
	private static RC4PrngCipher random = RC4PrngCipher.getInstance();

	public static BigInteger rsaep(RSAPublicKey key, BigInteger data) {
		return data.modPow(key.getPublicExponent(), key.getModulus());
	}

	public static BigInteger rsadp(RSAPrivateCrtKey key, BigInteger data) {
		BigInteger p = key.getPrimeP();
		BigInteger q = key.getPrimeQ();
		BigInteger dP = key.getPrimeExponentP();
		BigInteger dQ = key.getPrimeExponentQ();
		BigInteger qInv = key.getCrtCoefficient();
		BigInteger m1 = data.modPow(dP, p);
		BigInteger m2 = data.modPow(dQ, q);
		BigInteger h = m1.subtract(m2).multiply(qInv).mod(p);
		BigInteger m = q.multiply(h).add(m2);
		return m;
	}

	public static byte[] padSign(byte[] data, int blockSize) {
		byte[] padded = new byte[blockSize];
		padded[0] = 0;
		padded[1] = 1;
		for (int i = 2; i < blockSize - data.length - 1; i ++) {
			padded[i] = (byte) 0xff;
		}
		padded[blockSize - data.length - 1] = 0;
		System.arraycopy(data, 0, padded, blockSize - data.length, data.length);
		return padded;
	}

	public static byte[] padEncrypt(byte[] data, int blockSize) {
		byte[] padded = new byte[blockSize];
		padded[0] = 0;
		padded[1] = 2;
		random.output(padded, 2, blockSize - data.length - 3);
		for (int i = 2; i < blockSize - data.length - 1; i ++) {
			if (padded[i] == 0) {
				int posInt = Bytes.toInt(random.output(4)) & 0x7fffffff;
				padded[i] = (byte) (posInt % 255 + 1);
			}
		}
		padded[blockSize - data.length - 1] = 0;
		System.arraycopy(data, 0, padded, blockSize - data.length, data.length);
		return padded;
	}

	public static byte[] unpadDecrypt(byte[] padded) {
		int dataLen = 0;
		for (int i = 2; i < padded.length; i ++) {
			if (padded[i] == 0) {
				dataLen = padded.length - i - 1;
				break;
			}
		}
		byte[] data = new byte[dataLen];
		System.arraycopy(padded, padded.length - dataLen, data, 0, dataLen);
		return data;
	}

	public static byte[] unpadVerify(byte[] padded) {
		if (padded[0] != 0 || padded[1] != 1) {
			return Bytes.EMPTY_BYTES;
		}
		int dataLen = 0;
		for (int i = 2; i < padded.length; i ++) {
			if (padded[i] == 0) {
				dataLen = padded.length - i - 1;
				break;
			} else if (padded[i] != -1) {
				return Bytes.EMPTY_BYTES;
			}
		}
		byte[] data = new byte[dataLen];
		System.arraycopy(padded, padded.length - dataLen, data, 0, dataLen);
		return data;
	}

	public static int getBlockSize(RSAKey key) {
		return (key.getModulus().bitLength() + 7) / 8;
	}

	public static byte[] rsaep(RSAPublicKey key, byte[] data) {
		return Bytes.fromBigInteger(rsaep(key, new BigInteger(1, data)), getBlockSize(key));
	}

	public static byte[] rsadp(RSAPrivateCrtKey key, byte[] data) {
		return Bytes.fromBigInteger(rsadp(key, new BigInteger(1, data)), getBlockSize(key));
	}

	public static byte[] encrypt(RSAPublicKey key, byte[] data) {
		return rsaep(key, padEncrypt(data, getBlockSize(key)));
	}

	public static byte[] decrypt(RSAPrivateCrtKey key, byte[] data) {
		return unpadDecrypt(rsadp(key, data));
	}

	public static byte[] sign(RSAPrivateCrtKey key, byte[] data) {
		return rsadp(key, padSign(data, getBlockSize(key)));
	}

	public static byte[] verify(RSAPublicKey key, byte[] data) {
		return unpadVerify(rsaep(key, data));
	}
}